unit FileSync;

{$mode ObjFPC}{$H+}

{todo:
  1、考虑切换到AbZipper支持zip压缩包添加密码;
  2、文件数量多时，内存占用过多。}

//是否启用日志
{$Define LoggerActived}

interface

uses Classes, Windows, SysUtils, DaemonApp, zipper, ZStream, {ULZMACommon,} SevenZip,
  Contnrs, fileutil, DateUtils, logger, regexpr;

const
  TSyncDirectionName: array[0..2] of string = ('双向', '源到目标', '目标到源');
  TSyncDirectionSymbol: array[0..2] of string = ('←→', '→', '←');
  SYNC_EXT = '.~sync';//临时文件扩展名

type
  //文件同步方向：双向、从源目录到目标目录、从目标目录到源目录
  TSyncDirection = (sdBoth, sdSrcToDest, sdDestToSrc);
  //unc连接账号、密码
  TUncAccount = record
    Account,
    Password: array[0..50] of Char;
  end;

  //TSyncConfig = record
  TSyncConfig = class
    Section,            //对应配置文件的Section
    SrcDir: string;     //源目录
    SrcUncAccount: TUncAccount;
    DestDir: string;    //目标目录
    DestUncAccount: TUncAccount;
    Recurse: boolean;   //包含子目录，默认不包含
    Direction: TSyncDirection;//文件同步方向
    WorkTime: string;   //作业时间，格式：10:00-12:20。非作业时间不同步文件，没定义表示24小时
    Igonre: string;     //忽略的目录或文件，格式：[dir],[2021*],*.tmp,ab??.bak，其中目录用[]括住，支持通配符号，其中?适配1个字符，*适配多个字符
    Compress: string;   //将文件同步目标目录时进行压缩，会耗用更多CPU资源，支持zip和7z两种格式，不设定表示不进行压缩。
    Password: string;   //为压缩文件添加密码，只支持7z格式
    Pause: Word;        //完成同步后暂停多少秒再进行下一个循环，默认值5秒。最大值3600秒。

    WorkTimeBegin,      //作业时间起，格式：1000
    WorkTimeEnd: Word;  //作业时间止，格式：1220
    IgonreDirRegex: string;  //忽略的目录，由Igonre解释得到
    IgonreFileRegex: string ;//忽略文件的正式表达式，由Igonre解释得到
  end;

  { TFileAttr }

  TFileAttr = class
  public
    Path: string;             //存放路径
    FileName: string;         //文件名
    FileSize: Int64;          //文件大小
    LastWriteTime: TDateTime; //最后更新时间
    constructor Create(const Savepath, Afilename: string; Size: Int64; TimeStamp: TDateTime);
  end;
  PFileAttr = ^TFileAttr;

  { TFileSyncThread }

  TFileSyncThread = class(TThread)
  private
    Caller: TDaemon;
    SyncConfig: TSyncConfig;
    FZipper: TZipper;         //压缩对象
    FSourceFiles: TObjectList;//源目录文件清单
    FDestFiles:TObjectList;   //目标目录文件清单
    FSourceFileCount,
    FDestFileCount: Integer;
    {$IFDEF LoggerActived}
    FLogger: TLogger;         //日志对象
    {$EndIf}
    FAhead: Boolean;          //只处理当天的文件
    procedure WriteLog(const Text: string);
    //-----------------------------
    procedure ZipFile(const Filename, ZipFile: string; const FileDateTime: TDateTime);
    procedure UnZipFile(const Filename, DestPath: string);
    //procedure SevenZipFile(const Filename: string; const FileDateTime: TDateTime);
    //procedure LZMAFile(const Filename: string);
    function Get7ZCompressionlevel(const ext: string): Cardinal;
    function GetZipCompressionlevel(const ext: string): TCompressionlevel;
    Procedure OnZipProgress(Sender : TObject; Const Pct : Double);
    function On7ZProgress(sender: Pointer; total: boolean; value: Int64): HRESULT; stdcall;
    //procedure OnLZMAProgress (const Action: TLZMAProgressAction; const Value:int64);
    //-----------------------------
    procedure FindFiles(AList: TObjectList; const Dir: string; Recurse: Boolean);
    function IsIgonreFile(const Filename: string): boolean;
    function IsIgonreDirectory(const Dir: string): boolean;
  protected
    procedure Execute; override;
  public
    constructor Create(Sender: TDaemon; Config: TSyncConfig; Ahead: boolean);
    destructor Destroy; override;
  end;

  function IsUncDrive(const Drv: string): Boolean;
  function DecodeUncAccount(const path: string; var unc: TUncAccount): Boolean;
  function ConnectUnc(const unc, account, password: string): boolean;

implementation

uses FileSyncDeamon{, ULZMAEncoder, UBufferedFS};

function IsUncDrive(const Drv: string): Boolean;
begin
  Result:=
    (Length(Drv)>2) and
    (Drv[1]=PathDelim) and
    (Drv[2]=PathDelim);
end;

function DecodeUncAccount(const path: string; var unc: TUncAccount): Boolean;
var
    s, psw: string;
begin
    Result := false;
    FillMemory(@unc, SizeOf(TUncAccount), 0);
    if (path = '') or (path.IndexOf('@') = -1) then Exit;
    s := path.Substring(0, path.IndexOf('@'));
    if (s = '') or (s.IndexOf(':') = -1) then Exit;
    //unc.Account := s.Substring(0, s.IndexOf(':'));
    //unc.Password := s.Substring(s.IndexOf(':') + 1);
    psw := s.Substring(0, s.IndexOf(':'));
    lstrcpyn(unc.Account, @psw[1], psw.Length+1);
    psw := s.Substring(s.IndexOf(':') + 1);
    if (psw <> '') then
        psw := DecodePassword(psw);
    lstrcpyn(unc.Password, @psw[1], psw.Length+1);
end;

function ConnectUnc(const unc, account, password: string): boolean;
var
    nr: NETRESOURCE;
    RemoteName: array[0..MAX_PATH] of char;
begin
    FillMemory(@RemoteName, SizeOf(RemoteName), 0);
    if (unc <> '') then
        StrLCopy(RemoteName, PChar(unc), unc.IndexOf('\', 3));
    nr.dwType := RESOURCETYPE_ANY;
    nr.lpLocalName := nil;
    nr.lpRemoteName := RemoteName;//@unc[1];
    nr.lpProvider := nil;
    Result := NO_ERROR = WNetAddConnection2(nr, PChar(password), PChar(account), 0);
end;

function GetFileDateTime(Filename: string): TDateTime;
var
    sr: TSearchRec;
begin
    result := TDateTime(0);
    if SysUtils.FindFirst(Filename, faAnyFile, sr) = 0 then begin
        result := sr.TimeStamp;
        SysUtils.FindClose(sr);
    end;
end;

function SetFileDateTime(FileName: String; NewDateTime : TDateTime) : Boolean;
Var
    FileHandle: Integer;
    FileTime: TFileTime;
    FT: TFileTime;
    SysTime: TSystemTime;
Begin
    Result := False;
    try
        DecodeDate(NewDateTime, SysTime.wYear, SysTime.wMonth, SysTime.wDay);
        DecodeTime(NewDateTime, SysTime.wHour, SysTime.wMinute, SysTime.wSecond, SysTime.wMilliSeconds);
        if SystemTimeToFileTime(SysTime, FT) and LocalFileTimeToFileTime(FT, FileTime) Then Begin
            FileHandle := FileOpen(FileName, fmOpenReadWrite Or fmShareExclusive);
            Result := SetFileTime(FileHandle, @FileTime, @FileTime, @FileTime);
        End;
    finally
        FileClose(FileHandle);
    end;
End;

function TimeDiff(D1, D2: TDateTime): string;
var
    diff: TDateTime;
begin
    if (D1 > D2) then begin
        diff := D2;
        D2 := D1;
        D1 := diff;
    end;
    diff := D2 - D1;
    if (SecondSpan(D1, D2) < 60) then
      Result := FormatDateTime('s', diff) + ' 秒'
    else
      Result := FormatDateTime('hh:nn:ss', diff);
    if (diff > 1) then
      Result := IntToStr(DaysBetween(D1, D2)) + '天' + Result;
end;

//文件排序：最近的文件在前
function CompareLastWriteTime(Item1, Item2: Pointer): Integer;
begin
    Result := -Integer(CompareDateTime(TFileAttr(Item1).LastWriteTime, TFileAttr(Item2).LastWriteTime));
end;

{ TFileAttr }

constructor TFileAttr.Create(const Savepath, Afilename: string; Size: Int64;
  TimeStamp: TDateTime);
begin
    Self.Path := Savepath;
    Self.FileName := Afilename;
    Self.FileSize := Size;
    Self.LastWriteTime := TimeStamp;
end;

{ TFileSyncThread }

procedure TFileSyncThread.WriteLog(const Text: string);
begin
    {$IFDEF LoggerActived}
    FLogger.WriteLog(Text);
    {$ENDIF}
end;

procedure TFileSyncThread.ZipFile(const Filename, ZipFile: string; const FileDateTime: TDateTime);
var
    n: integer;
    Arch: I7zOutArchive;
begin
    if not FileExists(Filename) then Exit;

    //压缩前先删除同名文件
    n := 0;
    while FileExists(ZipFile) and (n < 10) do begin
        if (n > 0) then
          Sleep(300);
        DeleteFile(ZipFile);
        n += 1;
    end;
    //删除失败，退出
    if FileExists(ZipFile) then begin
        WriteLog(Format('文件已%s存在', [ZipFile]));
        raise EInOutError.Create(Format('文件已%s存在', [ZipFile]));
    end;

    //用SevenZip压缩zip，文件名为中文时会乱码。直接用Zipper则正常
    if SyncConfig.Compress  = 'zip' then begin
        FZipper.FileName := ZipFile;
        FZipper.Entries.Clear;
        FZipper.Entries.AddFileEntry(Filename, ExtractFileName(Filename))
            .CompressionLevel := GetZipCompressionlevel(ExtractFileExt(Filename));
        FZipper.ZipAllFiles;
        SetFileDateTime(ZipFile, FileDateTime);
        Exit;
    end;

    case SyncConfig.Compress of
        //'zip': begin
        //    Arch := CreateOutArchive(CLSID_CFormatZip);
        //    SetCompressionMethod(Arch, TZipCompressionMethod.mzBZip2);
        //end;
        '7z': begin
            Arch := CreateOutArchive(CLSID_CFormat7z);
            SevenZipSetCompressionMethod(Arch, T7zCompressionMethod.m7Deflate64);
        end;
        else Arch := nil;
    end;
    if Arch = nil then Exit;
    try
        Arch.SetProgressCallback(nil, @On7ZProgress);
        //Arch.SetPropertie('cu', 'on');
        Arch.SetPassword(UnicodeString(SyncConfig.Password));
        SetCompressionLevel(Arch, Get7ZCompressionlevel(ExtractFileExt(Filename)));
        Arch.AddFile(Filename, ExtractFileName(Filename));
        Arch.SaveToFile(ZipFile);
        SetFileDateTime(ZipFile, FileDateTime);
    finally
        Arch := nil;
    end;
end;

procedure TFileSyncThread.UnZipFile(const Filename, DestPath: string);
var
//  UnZipper: TUnZipper;
    Arch: I7zInArchive;
begin
    {if not FileExists(Filename) then Exit;

    UnZipper := TUnZipper.Create;
    try
        UnZipper.FileName := Filename;
        UnZipper.OutputPath := DestPath;
        //UnZipper.Examine;
        UnZipper.UnZipAllFiles;
    finally
        UnZipper.Free;
    end;}
    case SyncConfig.Compress of
        'zip':
            Arch := CreateInArchive(CLSID_CFormatZip);
        '7z':
            Arch := CreateInArchive(CLSID_CFormat7z);
        else Arch := nil;
    end;

    if (nil = Arch) then Exit;
    Arch.SetProgressCallback(nil, @On7ZProgress);
    Arch.OpenFile(Filename);
    Arch.ExtractTo(DestPath);
    Arch := nil;
end;

{procedure TFileSyncThread.LZMAFile(const Filename: string);
var
  inStream: TBufferedFS;
  outStream: TBufferedFS;
  n: integer;
  ZipName: string;
begin
    WriteLog('SevenZipFile:'+Filename);
    if not FileExists(Filename) then Exit;

    //压缩前先删除同名文件
    n := 0;
    ZipName := Filename + '.7z';
    while FileExists(ZipName) and (n < 10) do begin
        if (n > 0) then
          Sleep(300);
        DeleteFile(ZipName);
        n += 1;
    end;
    //删除失败，退出
    if FileExists(ZipName) then
        raise EInOutError.Create(Format('文件已%s存在', [ZipName]));

    inStream := TBufferedFS.Create(Filename, fmOpenRead or fmShareDenyNone);
    try
        outStream := TBufferedFS.Create(ZipName, fmCreate);
        try
            TLZMAEncoder(FZipper).Code(inStream, outStream, -1, -1);
        finally
            outStream.Free;
        end;
    finally
        inStream.Free;
    end;
end;}

{procedure TFileSyncThread.SevenZipFile(const Filename: string);
begin
end;}

function TFileSyncThread.Get7ZCompressionlevel(const ext: string): Cardinal;
begin
    case ext.ToLower() of
        '.jpg',  '.jpe',  '.jpeg', '.png',  '.gif',  '.webp',
        '.mp3',  '.mp4',  '.mov',  '.mkv',  '.flv',  '.flac',
        '.docx', '.xlsx', '.pptx', '.dotx', '.xltx', '.potx', '.ppsx',
        '.zip',  '.rar',  '.7z',   '.z',    '.tar',  '.xz',   '.gz',  '.tgz', '.tbz',
        '.arj',  '.lzh',  '.lha',  '.cab',  '.iso',  '.xar',  '.dmg', '.wim', '.swm',
        '.bz2',  '.rpm',  '.deb',  '.apk',  '.cpio', '.gzip', '.jar', '.xpi', '.lzma',
        '.mobi', '.epub'
          : Result := 0;
        else Result := 9;
    end;
end;

function TFileSyncThread.GetZipCompressionlevel(const ext: string): TCompressionlevel;
begin
    if (Get7zCompressionlevel(ext)) = 0 then
        Result := clnone
    else
        Result := clmax;
end;

procedure TFileSyncThread.OnZipProgress(Sender: TObject; const Pct: Double);
var
  n: word;
begin
    if Self.Terminated then begin
        FZipper.Terminate;

        //取消压缩时删除现存文件
        n := 0;
        while (n < 10) and (FZipper <> nil) and (FileExists(FZipper.FileName)) do begin
            DeleteFile(FZipper.FileName);
            Sleep(300);
        end;
    end
    else if (CPUUsage = 100) then
        //CPUUsage设定为0时，使用临界区单线处理文件
        Sleep(0)
    else
        Sleep(CPUUsage);
end;

function TFileSyncThread.On7ZProgress(sender: Pointer; total: boolean;
  value: Int64): HRESULT; stdcall;
begin
    if Self.Terminated then
        Result := S_False
    else begin
        Result := S_OK;
        //CPUUsage设定为0时，使用临界区单线处理文件
        if (CPUUsage = 100) then
            Sleep(0)
        else
            Sleep(CPUUsage);
    end;
end;

procedure TFileSyncThread.FindFiles(AList: TObjectList; const Dir: string;
  Recurse: Boolean);
var
    sr: TSearchRec;
    FToday: TDateTime;
  //判断处理当天的还是以前的
  function CompareTimeStamp: boolean;
  begin
    if FAhead then
      Result := sr.TimeStamp >= FToday
    else
      Result := sr.TimeStamp < FToday;
  end;
begin
    FToday := Trunc(Now);
    if SysUtils.FindFirst(Dir + '*.*', faAnyFile, sr) = 0 then
    begin
        repeat
              if (sr.Name <> '.') and (sr.Name <> '..') then
              begin
                  if (sr.Attr and faDirectory) <> 0 then begin
                      //递归处理目录
                      if (Recurse) and (not IsIgonreDirectory(sr.Name)) then
                          FindFiles(AList, IncludeTrailingPathDelimiter(Dir + sr.Name), Recurse);
                  end else begin
                      //非忽略的文件加到List中
                      if (not IsIgonreFile(sr.Name)) and (CompareTimeStamp) then begin
                          AList.Add(TFileAttr.Create(Dir, sr.Name, sr.Size, sr.TimeStamp));
                          //WriteLog(Dir + sr.Name);
                      end;
                  end;
              end;
              Sleep(0);
        until SysUtils.FindNext(sr) <> 0;
        SysUtils.FindClose(sr);
    end;
end;

function TFileSyncThread.IsIgonreFile(const Filename: string): boolean;
var
    reg: TRegExpr;
begin
    //忽略同步产生的临时文件
    if (LowerCase(ExtractFileExt(Filename)) = SYNC_EXT) then Exit(true);
    //根据配置判断是否忽略文件
    if (SyncConfig.IgonreFileRegex = '') then Exit(false);
    reg := TRegExpr.Create(SyncConfig.IgonreFileRegex);
    try
        Result := reg.Exec(Filename);
    finally
        reg.Free;
    end;
end;

function TFileSyncThread.IsIgonreDirectory(const Dir: string): boolean;
var
    reg: TRegExpr;
begin
    //根据配置判断是否忽略目录
    if (SyncConfig.IgonreDirRegex = '') then Exit(false);
    reg := TRegExpr.Create(SyncConfig.IgonreDirRegex);
    try
        Result := reg.Exec(Dir);
    finally
        reg.Free;
    end;
end;

{procedure TFileSyncThread.OnSevenZipProgress(const Action: TLZMAProgressAction;
  const Value: int64);
var
    n: word;
begin
    if Self.Terminated then begin
        //TLZMAEncoder(FZipper).finished := true;

        //n := 0;
        //while (n < 10) and (FZipper <> nil) and (FileExists(TLZMAEncoder(FZipper).FileName)) do begin
        //  DeleteFile(TLZMAEncoder(FZipper).FileName);
        //  Sleep(300);
        //end;
    end;
end; }

procedure TFileSyncThread.Execute;
  //判断文件是否需要复制
  function IsFileNeedCopy(src: TFileAttr; destfile: string): boolean;
  var
    lasttime: TDateTime;
  begin
      if not (FileExists(destfile)) then Exit(true);
      lasttime := GetFileDateTime(destfile);
      //文件时间相同不需要复制
      //FAT文件的时间精度为2秒、偶数对齐，判断文件时间是否相同时允许误差4秒
      if (SecondsBetween(src.LastWriteTime, lasttime) < 4) then Exit(false);
      Result := true;
  end;

  //从源目录向目标目录同步文件
  function SourceToDestination: UInt64;
  var
    i, errCode: integer;
    srcfile, destfile: string;
    isRetry: boolean;
  label
    retry;
  begin
    Result := 0;
    for i := 0 to FSourceFiles.Count - 1 do begin
      srcfile := SyncConfig.SrcDir + TFileAttr(FSourceFiles[i]).Path.Substring(SyncConfig.SrcDir.Length) + TFileAttr(FSourceFiles[i]).FileName;
      destfile := SyncConfig.DestDir + TFileAttr(FSourceFiles[i]).Path.Substring(SyncConfig.SrcDir.Length) + TFileAttr(FSourceFiles[i]).FileName;
      //需要压缩的，加上扩展名
      if SyncConfig.Compress <> '' then
        destfile += '.' + SyncConfig.Compress;

      isRetry := false;
      retry:
      try
        if (FileExists(srcfile)) and IsFileNeedCopy(TFileAttr(FSourceFiles[i]), destfile) then begin
          WriteLog('[→]源文件：' + srcfile);
          WriteLog('[→]目标文件：' + destfile);

          ForceDirectories(ExtractFilePath(destfile));
          //删除同名临时文件
          if (FileExists(destfile + SYNC_EXT)) then begin
            //去掉只读属性
            SetFileAttributes(PChar(destfile + SYNC_EXT), FILE_ATTRIBUTE_NORMAL);
            //删除
            DeleteFile(destfile + SYNC_EXT);
          end;
          //CPUUsage设定为0时，使用临界区单线处理文件，可降低CPU使用率和磁盘IO
          if (100 = CPUUsage) then begin
            if SyncConfig.Compress <> '' then
              EnterCriticalSection(CriticalForZip)
            else
              EnterCriticalSection(Critical);
          end;
          //复制或压缩时先写到临时文件，成功后再改名
          if SyncConfig.Compress <> '' then
            ZipFile(srcfile, destfile + SYNC_EXT, TFileAttr(FSourceFiles[i]).LastWriteTime)
          else
            CopyFile(srcfile, destfile + SYNC_EXT, [cffOverwriteFile, cffCreateDestDirectory, cffPreserveTime], true);
          //删除同名文件
          if (FileExists(destfile)) then begin
            //去掉只读属性
            SetFileAttributes(PChar(destfile), FILE_ATTRIBUTE_NORMAL);
            //删除
            DeleteFile(destfile);
          end;
          //改名
          RenameFile(destfile + SYNC_EXT, destfile);
          //CPUUsage设定为0时，使用临界区单线处理文件，可降低CPU使用率和磁盘IO
          if (100 = CPUUsage) then begin
            if SyncConfig.Compress <> '' then
              LeaveCriticalSection(CriticalForZip)
            else
              LeaveCriticalSection(Critical);
          end;
          Result += TFileAttr(FSourceFiles[i]).FileSize;
        end;
      except on Ex: Exception do begin
        errCode := GetLastOSError();
        WriteLog(Ex.ToString());
        WriteLog(Format('[→]%s复制到%s发生错误：(%d)%s', [srcfile, destfile, errCode, SysErrorMessage(errCode)]));
        //如果源目录是unc路径，尝试连接后再重试
        if (false = isRetry) and ((errCode = 161) or (errCode = 1450)) and (StrLen(SyncConfig.SrcUncAccount.Account) > 0) then begin
            isRetry := true;
            ConnectUnc(SyncConfig.SrcDir, SyncConfig.SrcUncAccount.Account, SyncConfig.SrcUncAccount.Password);
        end;
      end; end;
      //如果需要重试一次
      if (isRetry) then goto retry;
      if (Terminated) then
        break
        //CPUUsage设定为0时，使用临界区单线处理文件
      else if (100 = CPUUsage) then
        Sleep(0)
      else
        Sleep(CPUUsage);
    end;
  end;

  //从目标目录向源目录同步文件
  function DestinationToSource: UInt64;
  var
    i, errCode: integer;
    srcfile, destfile: string;
    isRetry: boolean;
  label
    retry;
  begin
    Result := 0;
    for i := 0 to FDestFiles.Count - 1 do begin
      srcfile := SyncConfig.SrcDir + TFileAttr(FDestFiles[i]).Path.Substring(SyncConfig.DestDir.Length) + TFileAttr(FDestFiles[i]).FileName;
      destfile := SyncConfig.DestDir + TFileAttr(FDestFiles[i]).Path.Substring(SyncConfig.DestDir.Length) + TFileAttr(FDestFiles[i]).FileName;
      //需要解压缩的，去掉压缩包扩展名
      if (SyncConfig.Compress <> '') and (LowerCase(ExtractFileExt(destfile)) = ('.' + SyncConfig.Compress)) then begin
        srcfile := srcfile.Substring(0, srcfile.Length - Length('.' + SyncConfig.Compress));
      end;

      isRetry := false;
      retry:
      try
        if (FileExists(destfile)) and IsFileNeedCopy(TFileAttr(FDestFiles[i]), srcfile) then begin
          WriteLog('[←]源文件：' + srcfile);
          WriteLog('[←]目标文件：' + destfile);

          ForceDirectories(ExtractFilePath(srcfile));
          if (FileExists(srcfile)) then begin
            //去掉只读属性
            SetFileAttributes(PChar(srcfile), FILE_ATTRIBUTE_NORMAL);
            //先删除文件再复制
            DeleteFile(srcfile);
          end;
          //CPUUsage设定为0时，使用临界区单线处理文件，可降低CPU使用率和磁盘IO
          if (100 = CPUUsage) then begin
            if (SyncConfig.Compress <> '') then
              EnterCriticalSection(CriticalForZip)
            else
              EnterCriticalSection(Critical);
          end;
          if SyncConfig.Compress <> '' then
            UnZipFile(destfile, ExtractFilePath(srcfile))
          else
            CopyFile(destfile, srcfile, [cffOverwriteFile, cffCreateDestDirectory, cffPreserveTime], true);
          //CPUUsage设定为0时，使用临界区单线处理文件，可降低CPU使用率和磁盘IO
          if (100 = CPUUsage) then begin
            if SyncConfig.Compress <> '' then
              LeaveCriticalSection(CriticalForZip)
            else
              LeaveCriticalSection(Critical);
          end;
          Result += TFileAttr(FDestFiles[i]).FileSize;
        end;
      except on Ex: Exception do begin
        errCode := GetLastOSError();
        WriteLog(Ex.ToString());
        WriteLog(Format('[←]%s复制到%s发生错误：(%d)%s', [destfile, srcfile, errCode, SysErrorMessage(errCode)]));
        //如果源目录是unc路径，尝试连接后再重试
        if (false = isRetry) and ((errCode = 161) or (errCode = 1450)) and (StrLen(SyncConfig.DestUncAccount.Account) > 0) then begin
            isRetry := true;
            ConnectUnc(SyncConfig.DestDir, SyncConfig.DestUncAccount.Account, SyncConfig.DestUncAccount.Password);
        end;
      end; end;
      //如果需要重试一次
      if (isRetry) then goto retry;
      if (Terminated) then
        break
        //CPUUsage设定为0时，使用临界区单线处理文件
      else if (100 = CPUUsage) then
        Sleep(0)
      else
        Sleep(CPUUsage);
    end;
  end;

var
    t1: TDateTime;
    size: UInt64;
    i: integer;
begin
    while (not Terminated) do begin
        //没指定工作时间段，即24小时均访问
        if (SyncConfig.WorkTimeBegin = SyncConfig.WorkTimeEnd)
          //指定了工作时间段，且在时间段内
          or (SyncConfig.WorkTimeBegin > 0) and (SyncConfig.WorkTimeEnd > 0)
          and (HourOf(Now)*100 + MinuteOf(Now) >= SyncConfig.WorkTimeBegin)
          and (HourOf(Now)*100 + MinuteOf(Now) <= SyncConfig.WorkTimeEnd)
        then
        try
            //连接unc资源
            if (strlen(SyncConfig.SrcUncAccount.Account) > 0) then begin
                if (ConnectUnc(SyncConfig.SrcDir, SyncConfig.SrcUncAccount.Account, SyncConfig.SrcUncAccount.Password)) then
                    WriteLog('[→]连接unc资源成功:' + SyncConfig.SrcDir)
                else begin
                    size := GetLastError;
                    if size <> ERROR_SESSION_CREDENTIAL_CONFLICT{1219} then
                        WriteLog(Format('[→]连接unc资源失败:%s(%d)%s', [SyncConfig.SrcDir, size, SysErrorMessage(size)]));
                end;
            end;
            if (StrLen(SyncConfig.DestUncAccount.Account) > 0) then begin
                if (ConnectUnc(SyncConfig.DestDir, SyncConfig.DestUncAccount.Account, SyncConfig.DestUncAccount.Password)) then
                    WriteLog('[←]连接unc资源成功:' + SyncConfig.DestDir)
                else begin
                    size := GetLastError;
                    if size <> ERROR_SESSION_CREDENTIAL_CONFLICT{1219} then
                        WriteLog(Format('[←]连接unc资源失败:%s(%d)%s', [SyncConfig.DestDir, size, SysErrorMessage(size)]));
                end;
            end;
            //同步源目录到目标目录
            if SyncConfig.Direction in [sdBoth, sdSrcToDest] then begin
                for i := FSourceFiles.Count - 1 downto 0 do
                    TFileAttr(FSourceFiles[i]).Free;
                FSourceFiles.Clear;
                FindFiles(FSourceFiles, SyncConfig.SrcDir, SyncConfig.Recurse);
                if (FSourceFiles.Count > 0) then begin
                    if (FSourceFileCount <> FSourceFiles.Count) then
                        WriteLog('[→]' + SyncConfig.SrcDir + '文件数量 ' + IntToStr(FSourceFiles.Count) + ' 个');
                    FSourceFiles.Sort(@CompareLastWriteTime);
                    //WriteLog('[→]排序完成');
                end;
                FSourceFileCount := FSourceFiles.Count;
                t1 := Now;
                size := SourceToDestination;
                if (size > 0) then
                    WriteLog(Format('[→]源到目标：共处理 %d 字节的文件，耗时 %s', [size, TimeDiff(t1, Now)]))
                ;//else
                //    WriteLog(Format('[→]源到目标，耗时 %s', [TimeDiff(t1, Now)]))
            end;
            if SyncConfig.Direction in [sdBoth, sdDestToSrc] then begin
                for i := FDestFiles.Count - 1 downto 0 do
                    TFileAttr(FDestFiles[i]).Free;
                FDestFiles.Clear;
                FindFiles(FDestFiles, SyncConfig.DestDir, SyncConfig.Recurse);
                if (FDestFiles.Count > 0) then begin
                    if (FDestFileCount <> FDestFiles.Count) then
                        WriteLog('[←]' + SyncConfig.DestDir + '文件数量 ' + IntToStr(FDestFiles.Count) + ' 个');
                    FSourceFiles.Sort(@CompareLastWriteTime);
                    //WriteLog('[←]排序完成');
                end;
                FDestFileCount := FDestFiles.Count;
                t1 := Now;
                size := DestinationToSource;
                if (size > 0) then
                    WriteLog(Format('[←]目标到源：共处理 %d 字节的文件，耗时 %s', [size, TimeDiff(t1, Now)]))
                ;//else
                //    WriteLog(Format('[←]目标到源，耗时 %s', [TimeDiff(t1, Now)]));
            end;
        Except on E: Exception do
            WriteLog(e.Message);
        end;
        if (not FAhead) then begin
            WriteLog('同步完成，本线程结束。');
            Self.Terminate;
        end
        else begin
            i := SyncConfig.Pause;
            while (i > 0) and (not Terminated) do begin
                Sleep(1000);
                i -= 1;
            end;
        end;
    end;
end;

constructor TFileSyncThread.Create(Sender: TDaemon; Config: TSyncConfig; Ahead: boolean);
var
    i: integer;
    arr: TStringArray;
    dir, files: string;
    function StrToRegex(const s : string): string;
    var
      n, m, x: integer;
    begin
      Result := '';
      if (s.IndexOfAny(['*', '?']) = -1) then
        Result := s
      else begin
        //查找?连续出现的次数
        n := 1;
        while n <= Length(s) do begin
          if s[n] <> '?' then begin
            if (CharInSet(s[n], ['!'..')', '+'..'.', ';'..'@', '['..'`', '{'..#255])) then
              Result += '\' + s[n]
            else
              Result += s[n];
            n += 1;
          end
          else begin
            //查找?连续出现的次数
            x := 1;
            for m := n+1 to Length(s) do
              if (s[m] <> '?') then
                break
              else
                x += 1;
            Result += Format('[^?]{%d}', [x]);
            n += x;
          end;
        end;
      end;
      Result := Result.Replace('*', '[^?]+');
      if (Result <> '') then
        Result := '^' + Result + '$';
    end;
begin
    FreeOnTerminate := true;
    Caller := Sender;
    FAhead := Ahead;
    FSourceFiles := TObjectList.Create(false);
    FDestFiles := TObjectList.Create(false);
    FSourceFileCount := 0;
    FDestFileCount := 0;
    {$IFDEF LoggerActived}
    FLogger := TLogger.Create(TFileSyncDeamon(Caller).MaxLogFileSize, Config.Section);
    {$EndIf}

    SyncConfig := TSyncConfig.Create;
    FillMemory(@SyncConfig.SrcUncAccount, SizeOf(TUncAccount), 0);
    FillMemory(@SyncConfig.DestUncAccount, SizeOf(TUncAccount), 0);
    SyncConfig.Section := Config.Section;
    SyncConfig.SrcDir := Config.SrcDir;
    //SyncConfig.SrcUncAccount.Account := Config.SrcUncAccount.Account;
    //SyncConfig.SrcUncAccount.Password := Config.SrcUncAccount.Password;
    lstrcpyn(SyncConfig.SrcUncAccount.Account, Config.SrcUncAccount.Account, StrLen(Config.SrcUncAccount.Account));
    lstrcpyn(SyncConfig.SrcUncAccount.Password, Config.SrcUncAccount.Password, StrLen(Config.SrcUncAccount.Password));
    SyncConfig.DestDir := Config.DestDir;
    //SyncConfig.DestUncAccount.Account := Config.DestUncAccount.Account;
    //SyncConfig.DestUncAccount.Password := Config.DestUncAccount.Password;
    lstrcpyn(SyncConfig.DestUncAccount.Account, Config.DestUncAccount.Account, StrLen(Config.DestUncAccount.Account));
    lstrcpyn(SyncConfig.DestUncAccount.Password, Config.DestUncAccount.Password, StrLen(Config.DestUncAccount.Password));
    SyncConfig.Recurse := Config.Recurse;
    SyncConfig.Direction := Config.Direction;
    SyncConfig.Compress := Config.Compress;
    SyncConfig.Password := Config.Password;
    SyncConfig.Pause := Config.Pause;
    SyncConfig.WorkTime := Config.WorkTime;
    //解释工作时间
    if (SyncConfig.WorkTime <> '') then begin
        arr := SyncConfig.WorkTime.Split(['-', ':']);
        SyncConfig.WorkTimeBegin := StrToInt(arr[0])*100 + StrToInt(arr[1]);
        SyncConfig.WorkTimeEnd := StrToInt(arr[2])*100 + StrToInt(arr[3]);
    end;

    //解释忽略项目
    SyncConfig.Igonre := Config.Igonre;
    arr := SyncConfig.Igonre.Split(',');
    dir := '';
    files := '';
    for i := 0 to Length(arr)-1 do begin
        arr[i] := arr[i].Trim();
        //跳过空白内容
        if (arr[i] = '') then Continue;
        //以[]括住的为目录
        if (arr[i].length>2) and (arr[i][1] = '[') and (arr[i][arr[i].length] = ']') then
            dir += StrToRegex(arr[i].Substring(1, arr[i].length-2)) + '|'
        else
            files += StrToRegex(arr[i]) + '|';
    end;
    if dir <> '' then
        SyncConfig.IgonreDirRegex := dir.Substring(0, dir.Length-1);//.Split(',');
    if files <> '' then
        SyncConfig.IgonreFileRegex := files.Substring(0, files.Length-1);//.Split(',');

    inherited Create(false);

    FZipper := nil;
    if '' <> SyncConfig.Compress then begin
       FZipper := TZipper.Create;
       FZipper.UseLanguageEncoding := true;
       FZipper.OnProgress := @OnZipProgress;
    end;
end;

destructor TFileSyncThread.Destroy;
var
    i: integer;
begin
    if (nil <> FZipper) then
       FreeAndNil(FZipper);

    for i := FSourceFiles.Count - 1 downto 0 do
        TFileAttr(FSourceFiles[i]).Free;
    FreeAndNil(FSourceFiles);

    for i := FDestFiles.Count - 1 downto 0 do
        TFileAttr(FDestFiles[i]).Free;
    FreeAndNil(FDestFiles);

    {$IFDEF LoggerActived}
    FLogger.Free;
    {$EndIf}
    SyncConfig.Free;

    inherited Destroy;
end;

end.

